%% @author Ian Wagner <bob@mochimedia.com>

%% @doc This is the main server for the Adventures of 5 Nachos Awesomeness

-module(nacho_server).
-author('Ian Wagner <ianthetechie@gmail.com>').

-include("ejson.hrl").
-include("db.hrl").
-include("socket_server.hrl").

-export([start/0, loop/3, send_party/1, push_message/1]).

-define(PORT, 7000).

start() ->
  mnesia:start(),
  db:init(),
  socket_server:start(?MODULE, ?PORT, {?MODULE, loop}, {?MODULE, push_message}).
  
loop(Socket, Name, Class) ->
  case gen_tcp:recv(Socket, 0) of
    {ok, Data} ->
      case catch ejson:decode(Data) of
        {invalid_json, Reason} ->
          io:format("Error: Invalid JSON. Reason:~p~n", [Reason]),
          MessageID = error,
          PlayerName = Name,
          PlayerClass = none,
          Success = false;
        {JSONData} ->
          MessageID = proplists:get_value(<<"message_id">>, JSONData),
          PlayerName = proplists:get_value(<<"name">>, JSONData),
          PlayerClass = proplists:get_value(<<"class">>, JSONData),
          ResponseMessageID = list_to_binary(string:concat(binary_to_list(MessageID), "_response")),
          case MessageID of
            <<"login">> ->
              ExistingPlayer = db:get_player(PlayerName),
              if
                (Name == <<"">>) and (ExistingPlayer == {atomic,[]}) ->
                  db:insert_player(PlayerName, PlayerClass),
                  send_map(Socket),
                  send_stats(Socket, PlayerName),
                  dispatcher ! update_party,
                  dispatcher ! update_monsters,
                  Success = true;
                true ->
                  Success = false
              end,
              gen_tcp:send(Socket, ejson:encode({[{<<"message_id">>, ResponseMessageID}, {<<"success">>, Success}]})),
              gen_tcp:send(Socket, <<"\n">>);
            <<"logout">> ->
              gen_tcp:send(Socket, ejson:encode({[{<<"message_id">>, ResponseMessageID}, {<<"success">>, true}]})),
              disconnected(Name, Socket),
              Success = true;
            <<"move">> ->
              Direction = string:to_upper(binary_to_list(proplists:get_value(<<"direction">>, JSONData))),
              case catch try_to_move(Name, Direction) of
                yes ->    %% Move/attack succeeded
                  dispatcher ! update_party,
                  %% TODO: Make the monsters move
                  dispatcher ! update_monsters,
                  Success = true;
                no ->     %% Move failed (walking into a wall, etc.)
                  Success = true;
                Error ->
                  io:format("~p~n", [Error]),
                  Success = false
              end;
            _ ->
              Success = false,
              throw({error, "Invalid Command"})
          end,
          gen_tcp:send(Socket, <<"\n">>)
      end,
      if    %% Check what kind of message we received
        (MessageID == <<"login">>) and Success -> loop(Socket, PlayerName, PlayerClass);
        MessageID /= <<"logout">> ->
          send_stats(Socket, Name),
          loop(Socket, Name, Class);
        true -> ok
      end;
    _ ->
      disconnected(Name, Socket)
  end.
  
push_message(State) ->
  receive
    {connect, Socket} ->
      NewState = State#server_state{sockets = [Socket | State#server_state.sockets]},
      push_message(NewState);
    update_party ->
      lists:map(fun(Sock) -> send_party(Sock) end, State#server_state.sockets),
      push_message(State);
    update_monsters ->
      lists:map(fun(Sock) -> send_monsters(Sock) end, State#server_state.sockets),
      push_message(State);
    {disconnect, Socket} ->
      NewState = State#server_state{sockets = lists:delete(Socket, State#server_state.sockets)},
      lists:map(fun(Sock) -> send_party(Sock) end, State#server_state.sockets),
      push_message(NewState)
  end.
  
disconnected(Name, Socket) ->
  db:remove_player(Name),
  io:format("~s disconnected~n", [Name]),
  dispatcher ! {disconnect, Socket},
  ok.

%%
%% Network dispatch helper functions
%%

send_map(Socket) ->
  {_, [Dungeon]} = db:get_dungeon(),
  gen_tcp:send(Socket, ejson:encode({[{<<"message_id">>, <<"dungeon_map">>}, {<<"map">>, Dungeon#dungeon.json}]})),
  gen_tcp:send(Socket, <<"\n">>).

send_party(Socket) ->
  {_, Players} = db:get_players(),
  gen_tcp:send(Socket, ejson:encode({[{<<"message_id">>, <<"party">>}, {<<"players">>, Players}]})),
  gen_tcp:send(Socket, <<"\n">>).

send_monsters(Socket) ->
  {_, Monsters} = db:get_monsters(),
  gen_tcp:send(Socket, ejson:encode({[{<<"message_id">>, <<"monsters">>}, {<<"monsters">>, Monsters}]})),
  gen_tcp:send(Socket, <<"\n">>).

send_stats(Socket, PlayerName) ->
  {_, [Player]} = db:get_player(PlayerName),
  PropList = {[{<<"damage">>, Player#player.damage},
               {<<"dexterity">>, Player#player.dexterity},
               {<<"dodge">>, Player#player.dodge},
               {<<"durability">>, Player#player.durability},
               {<<"max_durability">>, Player#player.max_durability},
               {<<"xp">>, Player#player.xp},
               {<<"level">>, Player#player.level}]},
  gen_tcp:send(Socket, ejson:encode({[{<<"message_id">>, <<"stats">>}, {<<"stats">>, PropList}]})),
  gen_tcp:send(Socket, <<"\n">>).

%% Attempts to move the player in the given direction
try_to_move(PlayerName, Direction) ->
  {_, [Dungeon]} = db:get_dungeon(),
  {_, [Player]} = db:get_player(PlayerName),
  X = Player#player.x,
  Y = Player#player.y,
  case Direction of
    "N" ->
      X1 = X,
      Y1 = Y - 1,
      Tile = get_dungeon_tile(Dungeon, Y1, X1);
    "S" ->
      X1 = X,
      Y1 = Y + 1,
      Tile = get_dungeon_tile(Dungeon, Y1, X1);
    "E" ->
      X1 = X + 1,
      Y1 = Y,
      Tile = get_dungeon_tile(Dungeon, Y1, X1);
    "W" ->
      X1 = X - 1,
      Y1 = Y,
      Tile = get_dungeon_tile(Dungeon, Y1, X1);
    _ ->
      X1 = X,
      Y1 = Y,
      Tile = "#"
  end,
  {_, Result} = db:get_monster([X1, Y1]),
  case Tile of
    "" ->
      if
        Result /= [] ->
          [Monster] = Result,
          if
            Monster#monster.damage == 0 ->
              PartySize = db:get_party_size(),
              io:format("Rolling a monster for ~w party members~n", [PartySize]),
              Level = Player#player.level,
              UpdatedMonster = Monster#monster{damage = random:uniform(7 * PartySize) + 13 + Level,
                                               dexterity = random:uniform(7 * PartySize) + 13 + Level,
                                               dodge = random:uniform(7 * PartySize) + 13 + Level,
                                               durability = random:uniform(7 * PartySize) + 13 + Level,
                                               level = Level},
              io:format("Monster created with ~w durability~n", [UpdatedMonster#monster.durability]),
              db:save_monster(UpdatedMonster),
              do_combat_player(Player, UpdatedMonster);
            true ->
              do_combat_player(Player, Monster)
          end,
          yes;
        true ->
          db:save_player(Player#player{x = X1, y = Y1}),
          yes
      end;
    _ ->
      no
  end.

get_dungeon_tile(Dungeon, Row, Col) ->
  {Map} = Dungeon#dungeon.json,
  TargetRow = proplists:get_value(list_to_binary(integer_to_list(Row)), Map),
  %%io:format("Row: ~w, Col: ~w~n", [Row, Col]),
  if
    TargetRow /= undefined ->
      %%io:format("TargetRow: ~w ~w~n", [Row, TargetRow]),
      {RowData} = TargetRow,
      TargetCell = proplists:get_value(list_to_binary(integer_to_list(Col)), RowData),
      if
        TargetCell /= undefined ->
          {CellData} = TargetCell,
          %%io:format("TargetCell: ~w ~w~n", [Col, CellData]),
          binary_to_list(proplists:get_value(<<"ascii">>, CellData, ""));
        true -> ""
      end;
    true -> ""
  end.

%% aD = Combatant A dexterity, bD = Combatant B dodge, output = Combatant A adjusted dexterity
adjusted_dexterity(ADex, BDodge) ->
  if
    ADex > BDodge * 2 ->
      BDodge * 2 - 1;
    ADex < BDodge / 2 ->
      trunc(BDodge / 2) + 1;
    true -> ADex
  end.

%% Performs combat round for the player
do_combat_player(Player, Monster) ->
  %% Check for Player hit
  MonsterDodge = Monster#monster.dodge,
  PlayerDex = adjusted_dexterity(Player#player.dexterity, Monster#monster.dodge),
  Hit = random:uniform(100) > ((1 - math:cos((0.25 - PlayerDex / (MonsterDodge * 2)) * 1.25 * math:pi())) / 2 * 100),
  if
    Hit ->
      %% It's a hit. Now to calculate the damage...
      UpdatedMonster = Monster#monster{durability=Monster#monster.durability - Player#player.damage},
      io:format("~s hit for ~w damage~n", [Player#player.name, Player#player.damage]),
      if
        UpdatedMonster#monster.durability =< 0 ->
          db:kill_monster(Monster#monster.position),
          dispatcher ! update_monsters;
        true ->
          db:save_monster(UpdatedMonster),
          dispatcher ! update_monsters,
          do_combat_monster(UpdatedMonster, Player)
      end;
    true ->
      do_combat_monster(Monster, Player)
  end.

%% Perform the monster's combat round
do_combat_monster(Monster, Player) ->
  %% Check for Monster hit
  MonsterDex = adjusted_dexterity(Monster#monster.dexterity, Player#player.dodge),
  PlayerDodge = Player#player.dodge,
  Hit = random:uniform(100) > ((1 - math:cos((0.25 - MonsterDex / (PlayerDodge * 2)) * 1.25 * math:pi())) / 2 * 100),
  if
    Hit ->
      %% It's a hit. Now to calculate the damage...
      UpdatedPlayer = Player#player{durability=Player#player.durability - Monster#monster.damage},
      if
        UpdatedPlayer#player.durability =< 0 ->
          db:remove_player(Player#player.name),
          io:format("~s died~n", [Player#player.name]),
          ok;
        true ->
          io:format("~s took ~w damage~n", [Player#player.name, Monster#monster.damage]),
          db:save_player(UpdatedPlayer),
          ok
      end;
    true -> ok
  end.